//==============================================================================
//
//         DJPEGImage - the JPEG image class in the ofc-library
//
//               Copyright (C) 2007  Dick van Oudheusden
//  
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 3 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free
// Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//
//==============================================================================
// 
//  $Date: 2008-08-20 17:16:29 $ $Revision: 1.4 $
//
//==============================================================================

#include <setjmp.h>

#include "ofc/DJPEGImage.h"

#include "ofc/DData.h"

#ifdef HAVE_DJPEGIMAGE
#include "jpeglib.h"
#endif


#if _INTERFACE_

#include "ofc/config.h"

#include "ofc/DImage.h"
#include "ofc/DDataReadable.h"
#include "ofc/DDataWritable.h"

#if defined(HAVE_LIBJPEG) && defined(HAVE_JPEGLIB_H)
#define HAVE_DJPEGIMAGE 1
#endif

#ifdef HAVE_DJPEGIMAGE

//
// The DJPEGImage class implements methods for reading and writing JPEG images.
// 
// @example
// #include <stdio.h>
// #include "ofc/DJPEGImage.h"
// #include "ofc/DFile.h"
// 
// int main(int argc, char *argv[])
// {
//   DJPEGImage *image = [DJPEGImage new];
//   DFile      *file  = [DFile new];
// 
//   char       name[] = "example.jpg";
// 
//   if ([file open :name :"rb"])
//   {
//     if ([DJPEGImage isImage :file])
//     {
//       if ([image open :file])
//       {
//         printf("\"%s\" is a JPEG image with width:%d height:%d BytesPerPixel:%d Images:%d\n",
//             name, [image width], [image height], [image bytesPerPixel], [image images]);
//         
//         [image close];
//       }
//       else
//         printf("Could not open image \"%s\".\n", name);
//     }
//     else
//       printf("\"%s\" is not a JPEG image.\n", name);
// 
//     [file close];
//   }
//   else
//     printf("Could not find \"%s\":%d\n", name, [file error]);
// 
//   [image free];                        // Cleanup
//   [file  free];
//   
//   return 0;
// }
// 

@interface DJPEGImage : DObject <DImage>
{
@private
  void                *_jinfo;         // the jpeg info
  void                *_jerr;          // the jpeg error
  unsigned             _height;        // the height of the image
  unsigned             _width;         // the width of the image
  unsigned             _bpp;           // the bytes per pixel
  BOOL                 _reading;       // reading or writing an image ?
  BOOL                 _error;         // is an error active ?
}

#endif
#endif



#ifdef HAVE_DJPEGIMAGE

#ifdef _PRIVATE_

#define DJPEG_BUFFER_SIZE      4096

typedef struct
{
  struct
  jpeg_source_mgr       pub;                    // public fields
  id <DDataReadable>    src;                    // source
  JOCTET                buf[DJPEG_BUFFER_SIZE]; // buffer
} _DJPEGSrcMgr;


typedef struct
{
  struct 
  jpeg_destination_mgr  pub;                    // public fields
  id <DDataWritable>    dst;                    // destination 
  JOCTET                buf[DJPEG_BUFFER_SIZE]; // buffer
} _DJPEGDstMgr;


typedef struct
{
  struct
  jpeg_error_mgr        pub;                    // public fields
  jmp_buf               jmp;                    // return
} _DJPEGErrMgr;


//// DJPEG error callback functions

//
// Do error exit
// 
// @param jinfo    the jpeg info
// 
static void _errorExit(j_common_ptr jinfo)
{
  _DJPEGErrMgr *mgr = (_DJPEGErrMgr *) jinfo->err;
  
  longjmp(mgr->jmp, 1);
}


//
// Output message
// 
// @param jinfo    the jpeg info
// 
//
static void _outputMessage(j_common_ptr jinfo)
{
  char buffer[JMSG_LENGTH_MAX];
  
  (*jinfo->err->format_message) (jinfo, buffer);

  WARNING(DW_UNKNOWN_WARNING, buffer);
}

//// DJPEG reading callback functions

//
// Initialise the source buffer for every image
// 
// @param jinfo    the jpeg info
// 
static void _initSource(j_decompress_ptr jinfo)
{
  // Nothing to do
}


//
// Fill the input buffer
// 
// @param jinfo    the jpeg info
// 
// @return success
// 
static boolean _fillInputBuffer(j_decompress_ptr jinfo)
{
  _DJPEGSrcMgr *mgr = (_DJPEGSrcMgr *) jinfo->src;
  
  size_t     nbytes = [mgr->src readData :mgr->buf :DJPEG_BUFFER_SIZE];

  if (nbytes <= 0) 
  {
    mgr->buf[0] = (JOCTET) 0xFF;     // Insert a fake EOI marker
    mgr->buf[1] = (JOCTET) JPEG_EOI;
    nbytes = 2;
  }

  mgr->pub.next_input_byte = mgr->buf;
  mgr->pub.bytes_in_buffer = nbytes;

  return TRUE;
}


//
// Skip input data
// 
// @param jinfo    the jpeg info
// @param nbyte    the number of bytes to skip
// 
static void _skipInputData(j_decompress_ptr jinfo, long nbytes)
{
  _DJPEGSrcMgr *mgr = (_DJPEGSrcMgr *) jinfo->src;

  if (nbytes > 0) 
  {
    while (nbytes > (long) mgr->pub.bytes_in_buffer) 
    {
      nbytes -= (long) mgr->pub.bytes_in_buffer;
      
      _fillInputBuffer(jinfo);
    }
    mgr->pub.next_input_byte += (size_t) nbytes;
    mgr->pub.bytes_in_buffer -= (size_t) nbytes;
  }
}


//
// Terminate the input source
// 
// @param jinfo    the jpeg info
// 
static void _termSource(j_decompress_ptr jinfo)
{
  // Nothing to do
}


//// DJPEG writing callback functions

//
// Initialise the destination buffer
// 
// @param jinfo      the jpeg info
// 
static void _initDestination(j_compress_ptr jinfo)
{
  _DJPEGDstMgr *mgr = (_DJPEGDstMgr *) jinfo->dest;
  
  mgr->pub.next_output_byte = mgr->buf;
  mgr->pub.free_in_buffer   = DJPEG_BUFFER_SIZE;
}

//
// Empty the output buffer
// 
// @param jinfo    the jpeg info
// 
// @return completion
// 
static boolean _emptyOutputBuffer (j_compress_ptr jinfo)
{
  _DJPEGDstMgr *mgr = (_DJPEGDstMgr *) jinfo->dest;

  [mgr->dst writeData :mgr->buf :DJPEG_BUFFER_SIZE];

  mgr->pub.next_output_byte = mgr->buf;
  mgr->pub.free_in_buffer   = DJPEG_BUFFER_SIZE;

  return TRUE;
}

//
// Terminate destination buffer
// 
// @param jinfo    the jpeg info
// 
static void _termDestination (j_compress_ptr jinfo)
{
  _DJPEGDstMgr *mgr = (_DJPEGDstMgr *) jinfo->dest;
  
  size_t nbytes = DJPEG_BUFFER_SIZE - mgr->pub.free_in_buffer;

  /* Write any data remaining in the buffer */
  if (nbytes > 0) 
  {
    [mgr->dst writeData :mgr->buf :nbytes];
  }

  [mgr->dst flush];
}

#endif


@implementation DJPEGImage

//
// Initialise a JPEG image object
//
// @return the object
//
- (DJPEGImage *) init
{
  [super init];

  _jinfo         = NULL;
  _jerr          = NULL;
  _height        = 0;
  _width         = 0;
  _bpp           = 4;
  _reading       = YES;
  _error         = NO;

  return self;
}


//// Copy related methods

//
// Do a shallow copy of a JPEG image (not implemented)
//
// @return the object
//
- shallowCopy
{
  WARNING(DW_METHOD_NOT_IMPL, "shallowCopy");

  return [super shallowCopy];
}


//// Deconstructor

//
// Free the image object
// 
// @return the object
// 
- free
{
  if (_jinfo != NULL)
  {
    [self close];
  }
  
  return [super free];
}



//// Image members

//
// Return the height of the image
// 
// @return the height
// 
- (unsigned) height
{
  return _height;
}

//
// Return the width of the image
// 
// @return the width
// 
- (unsigned) width
{
  return _width;
}

//
// Return the bytes per pixel (3: RGB or 4: RGBA)
// 
// @return the number of bytes
// 
- (unsigned) bytesPerPixel
{
  return _bpp;
}


//
// Return the number of images in the image (jpeg: always 1)
// 
// @return the number of images
// 
- (unsigned) images
{
  return 1;
}


//// Image methods

//
// Check if an image is an JPEG image
// 
// @param source        the source of the image
// 
// @return is it ?
// 
+ (BOOL) isImage :(id <DDataReadable>) source
{
  BOOL image = NO;
  
  if (source == nil)
  {
    WARNING(DW_INVALID_ARG, "source");
  }
  else
  {
    unsigned char data[4];
    unsigned long current = [source tell];
    unsigned long length  = 0;
    
    [source seek :0 :0];
    
    length = [source readData :data :sizeof(data) / sizeof(data[0])];
    
    image = ((data[0] == 0xFF) && (data[1] == 0xD8) && (data[2] == 0xFF) && (data[3] == 0xE0));
    
    [source seek :current :0];
  }
  
  return image;
}


//
// Open the JPEG image object for reading
// 
// @param source        the source with the image
//
// @return success
//
- (BOOL) open :(id <DDataReadable>) source
{
  BOOL ok = NO;
  
  if (source == nil)
  {
    WARNING(DW_INVALID_ARG, "source");
  }
  else if ((_jinfo != NULL) && (!_reading))
  {
    WARNING(DW_INVALID_STATE, "close");
  }
  else
  {
    j_decompress_ptr jinfo = NULL;
    _DJPEGSrcMgr    *mgr   = NULL;
    _DJPEGErrMgr    *jerr  = NULL;
    
    if (_jinfo != NULL)
    {
      [self close];
    }
    
    jinfo  = (j_decompress_ptr) objc_malloc(sizeof(struct jpeg_decompress_struct));
    jerr   =   (_DJPEGErrMgr *) objc_malloc(sizeof(_DJPEGErrMgr));
      
    _jerr  = jerr;
    _jinfo = jinfo;
    
    // Error structure
    jinfo->err = jpeg_std_error(&jerr->pub);
      
    jerr->pub.error_exit     = _errorExit;
    jerr->pub.output_message = _outputMessage;
    
    ok       = YES;
    if (setjmp(jerr->jmp))
    {
      ok     = NO;
    }

    if (ok)
    {
      jpeg_create_decompress(jinfo);
        
      mgr = (_DJPEGSrcMgr *) (*jinfo->mem->alloc_small)((j_common_ptr) jinfo, JPOOL_PERMANENT, sizeof(_DJPEGSrcMgr));

      // Decompress structure
      jinfo->src  = (struct jpeg_source_mgr *) mgr;
    
      mgr->pub.init_source       = _initSource;
      mgr->pub.fill_input_buffer = _fillInputBuffer;
      mgr->pub.skip_input_data   = _skipInputData;
      mgr->pub.resync_to_restart = jpeg_resync_to_restart; // use default method
      mgr->pub.term_source       = _termSource;
      mgr->pub.bytes_in_buffer   = 0;
      mgr->pub.next_input_byte   = NULL;
      mgr->src                   = source;
    
      _reading = YES;
      _error   = NO;
    
      jpeg_read_header(jinfo, TRUE);

      if (jinfo->num_components != 4) //RGB
      {
        jinfo->out_color_space = JCS_RGB;
        jinfo->quantize_colors = FALSE;
        
        jpeg_calc_output_dimensions(jinfo);
        
        _width  = (unsigned) jinfo->image_width;
        _height = (unsigned) jinfo->image_height;
        _bpp    = 3;
        
        jpeg_start_decompress(jinfo);
        
        // printf("Num comp: %d Width: %d Height: %d\n", jinfo->num_components, _width, _height);
      }
      else
      {
        WARNING(DW_UNKNOWN_WARNING, "DJPEGImage does not support CMYK images");
        
        ok = NO;
      }
    }

    if (!ok)
    {
      _error = YES;
      
      [self close];
    }
  }
  return ok;
}


//
// Open the JPEG image object for writing (RGB = 3 bytesPerPixel, no alpha)
// 
// @param destination        the file for the image
// @param width              the width of the image
// @param height             the height of the image
// @param quality            the quality [0..100]
//
// @return succes
//
- (BOOL) open :(id <DDataWritable>) destination :(unsigned) width :(unsigned) height :(int) quality
{
  BOOL ok = NO;
  
  if (destination == nil)
  {
    WARNING(DW_INVALID_ARG, "destination");
  }
  else if ((width == 0) || (height == 0))
  {
    WARNING(DW_INVALID_ARG, "width/height");
  }
  else if ((quality < 0) || (quality > 100))
  {
    WARNING(DW_INVALID_ARG, "quality");
  }
  else
  {
    j_compress_ptr jinfo = NULL;
    _DJPEGDstMgr  *mgr   = NULL;
    _DJPEGErrMgr  *jerr  = NULL;

    if (_jinfo != NULL)
    {
      [self close];
    }

    jinfo  = (j_compress_ptr) objc_malloc(sizeof(struct jpeg_compress_struct));
    jerr   =  (_DJPEGErrMgr *) objc_malloc(sizeof(_DJPEGErrMgr));
      
    _jerr  = jerr;
    _jinfo = jinfo;
    
    // Error structure
    jinfo->err = jpeg_std_error(&jerr->pub);
      
    jerr->pub.error_exit     = _errorExit;
    jerr->pub.output_message = _outputMessage;
    
    ok = YES;
    if (setjmp(jerr->jmp))
    {
      ok = NO;
    }

    if (ok)
    {
      jpeg_create_compress(jinfo);
        
      mgr = (_DJPEGDstMgr *) (*jinfo->mem->alloc_small)((j_common_ptr) jinfo, JPOOL_PERMANENT, sizeof(_DJPEGDstMgr));

      // Compress structure
      jinfo->dest = (struct jpeg_destination_mgr *) mgr;
    
      mgr->pub.init_destination    = _initDestination;
      mgr->pub.empty_output_buffer = _emptyOutputBuffer;
      mgr->pub.term_destination    = _termDestination;
      mgr->dst                     = destination;
    
      _reading = NO;
      _error   = NO;    
    
      jinfo->image_width  = width;
      jinfo->image_height = height;
      jinfo->input_components = 3;
      jinfo->in_color_space = JCS_RGB;
      
      jpeg_set_defaults(jinfo);
      
      jpeg_set_quality(jinfo, quality, TRUE);
      
      jpeg_start_compress(jinfo, TRUE);
    }

    if (ok)
    {
      _width  = width;
      _height = height;
      _bpp    = 3;
    }
    else
    {
      _error = YES;
      
      [self close];
    }
  }

  return ok;
}


//
// Get the next row of image data from the jpgeg file (format: RGB)
// 
// @param data          the image data for the next line (size = _width * _bpp)
//
// @return the number of pixels in the row (or 0 for error)
//
- (unsigned) readRow :(unsigned char *) data
{
  unsigned pixels = 0;
  
  if (data == NULL)
  {
    WARNING(DW_INVALID_ARG, "data");
  }
  else if ((_jinfo == NULL) || (!_reading))
  {
    WARNING(DW_INVALID_STATE, "open:");
  }
  else
  {
    _DJPEGErrMgr    *jerr  =   (_DJPEGErrMgr *) _jerr;
    j_decompress_ptr jinfo = (j_decompress_ptr) _jinfo;
    

    if (jinfo->output_scanline < jinfo->output_height)
    {
      JSAMPROW row[1];

      pixels = _width;
      
      if (setjmp(jerr->jmp))
      {
        pixels = 0;
        _error = YES;
      }

      if (pixels > 0)
      {
        row[0] = (JSAMPROW) data;
        
        jpeg_read_scanlines(jinfo, row, 1);
      }
    }
    else
    {
      WARNING(DW_INVALID_STATE, "image_height");
    }
  }
    
  return pixels;
}

//
// Set the next row of image data in the jpeg file (format: RGB)
// 
// @param data          the image data for the next line (size = _width * _bpp)
// @param width         the number of pixels in the row
//
// @return success
//
- (BOOL) writeRow :(unsigned char *) data :(unsigned) width
{
  BOOL ok = NO;
  
  if (data == NULL)
  {
    WARNING(DW_INVALID_ARG, "data");
  }
  else if ((_jinfo == NULL) || (_reading))
  {
    WARNING(DW_INVALID_STATE, "open:");
  }
  else if (width != _width)
  {
    WARNING(DW_INVALID_ARG, "width");
  }
  else
  {
    _DJPEGErrMgr    *jerr  = (_DJPEGErrMgr *) _jerr;
    j_compress_ptr   jinfo = (j_compress_ptr) _jinfo;
    

    if (jinfo->next_scanline < jinfo->image_height)
    {
      JSAMPROW row[1];
      
      row[0] = data;
      
      ok = YES;
      if (setjmp(jerr->jmp))
      {
        ok     = NO;
        _error = YES;
      }

      if (ok)
      {
        jpeg_write_scanlines(jinfo, row, 1);
      }
    }
    else
    {
      WARNING(DW_INVALID_STATE, "image_height");
    }
  }
    
  return ok;
}

//
// Close the image object
// 
// @return success
//
- (BOOL) close
{
  if (_jinfo != NULL)
  {
    if (_reading)
    {
      j_decompress_ptr jinfo = (j_decompress_ptr) _jinfo;
    
      if (!_error)
      {
        jpeg_finish_decompress(jinfo);
      }
      jpeg_destroy_decompress(jinfo);
    }
    else
    {
      j_compress_ptr jinfo = (j_compress_ptr) _jinfo;
      
      if (!_error)
      {
        jpeg_finish_compress(jinfo);
      }
      jpeg_destroy_compress(jinfo);
    }
    objc_free(_jinfo);
    objc_free(_jerr);
    
    _jinfo = NULL;
    _jerr  = NULL;
  }
  
  _height        = 0;
  _width         = 0;
  _bpp           = 3;
  _error         = NO;
  
  return YES;
}

@end

#endif

/*===========================================================================*/
